﻿using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Security.Principal;
using System.Text.RegularExpressions;
using System.Web;
using System.Web.Mvc;
using System.Web.Security;
using System.Web.UI;
using MyStack.Models;
using System.IO;
using System.Text;

namespace MyStack.Controllers
{

	[HandleError]
	public class AccountController : Controller
	{

		// This constructor is used by the MVC framework to instantiate the controller using
		// the default forms authentication and membership providers.

		public AccountController()
			: this(null, null)
		{
		}

		// This constructor is not used by the MVC framework but is instead provided for ease
		// of unit testing this type. See the comments at the end of this file for more
		// information.
		public AccountController(IFormsAuthentication formsAuth, IMembershipService service)
		{
			FormsAuth = formsAuth ?? new FormsAuthenticationService();
			MembershipService = service ?? new AccountMembershipService();
		}

		public IFormsAuthentication FormsAuth
		{
			get;
			private set;
		}

		public IMembershipService MembershipService
		{
			get;
			private set;
		}

		public ActionResult LogOn()
		{

			return View();
		}

		[AcceptVerbs(HttpVerbs.Post)]
		[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1054:UriParametersShouldNotBeStrings",
			Justification = "Needs to take same parameter type as Controller.Redirect()")]
		public ActionResult LogOn(string userName, string password, bool rememberMe, string returnUrl)
		{

			if (!ValidateLogOn(userName, password))
			{
				return View();
			}

			FormsAuth.SignIn(userName, rememberMe);
			if (!String.IsNullOrEmpty(returnUrl))
			{
				return Redirect(returnUrl);
			}
			else
			{
				return RedirectToAction("Index", "Home");
			}
		}

		public ActionResult LogOff()
		{

			FormsAuth.SignOut();

			return RedirectToAction("Index", "Home");
		}

		public ActionResult Register()
		{

			ViewData["PasswordLength"] = MembershipService.MinPasswordLength;

			return View();
		}

		[AcceptVerbs(HttpVerbs.Post)]
		public ActionResult Register(string userName, string email, string password, string confirmPassword)
		{

			ViewData["PasswordLength"] = MembershipService.MinPasswordLength;

			if (ValidateRegistration(userName, email, password, confirmPassword))
			{
				// Attempt to register the user
				MembershipCreateStatus createStatus = MembershipService.CreateUser(userName, password, email);

				if (createStatus == MembershipCreateStatus.Success)
				{
                    // Add the user to MyStack.User table
                    MyStackRepository db = new MyStackRepository();

					User user = new User();
					user.Name = userName;
					user.Email = email;

					//	Read the image from the avatar upload
					//	and store it in the Icon field
					byte[] image = null;

					if (Request.Files.Count == 1)
					{
						HttpPostedFileBase avatarUpload = (Request.Files[0] as HttpPostedFileBase);

						MemoryStream memStream = new MemoryStream();

						// Read the file from the input stream to 
						//	a byte array
						byte[] buffer = new byte[4096];

						while (avatarUpload.InputStream.Read(buffer, 0, buffer.Length) > 0)
						{
							memStream.Write(buffer, 0, buffer.Length);
						}
						//image = memStream.ToArray();

                        // Convert stream to Image
                        System.Drawing.Image avImage = System.Drawing.Image.FromStream(memStream);

                        // Determine the best height and width to use
                        int newHeight, newWidth, bigSide = 0;
                        int maxSide = 200; // boundary, want image size <=200x200

                        //determine if landscape or portrait
                        if (avImage.Width >= avImage.Height)
                        {
                            bigSide = avImage.Width;
                        }
                        else
                        {
                            bigSide = avImage.Height;
                        }
                        if (bigSide > maxSide)
                        {
                            //set new width and height
                            double dblCoef = maxSide / (double)bigSide;
                            newWidth = Convert.ToInt32(dblCoef * avImage.Width);
                            newHeight = Convert.ToInt32(dblCoef * avImage.Height);
                        }
                        else
                        {
                            newWidth = avImage.Width;
                            newHeight = avImage.Height;
                        }
                        // Create a thumbnail from the image
                        System.Drawing.Image tnImage = avImage.GetThumbnailImage(newWidth, newHeight, null, new System.IntPtr());

                        // Make a memory stream to work with the new thumbnail bytes
                        MemoryStream tnStream = new MemoryStream();
                        // Put the thumbnail bytes into the memory stream
                        tnImage.Save(tnStream, System.Drawing.Imaging.ImageFormat.Jpeg);
                        // Rewind the memory stream
                        tnStream.Position = 0;
                        // Load the byte array with the image
                        image = new Byte[tnStream.Length];
                        tnStream.Read(image, 0, (int)tnStream.Length);
                        // Return byte array to caller with image type
                        Response.ContentType = "image/jpeg";
                        Response.BinaryWrite(image);

					}
                 
					user.Icon = image;

					int userID = db.RegisterUser(user);
                    // Create corresponding stack
                    db.CreateStack(userID);
                    
					FormsAuth.SignIn(userName, false /* createPersistentCookie */);
					return RedirectToAction("Index", "Home");
				}
				else
				{
					ModelState.AddModelError("_FORM", ErrorCodeToString(createStatus));
				}
			}

			// If we got this far, something failed, redisplay form
			return View();
		}

		[Authorize]
		public ActionResult ChangePassword()
		{

			ViewData["PasswordLength"] = MembershipService.MinPasswordLength;

			return View();
		}

		[Authorize]
		[AcceptVerbs(HttpVerbs.Post)]
		[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes",
			Justification = "Exceptions result in password not being changed.")]
		public ActionResult ChangePassword(string currentPassword, string newPassword, string confirmPassword)
		{

			ViewData["PasswordLength"] = MembershipService.MinPasswordLength;

			if (!ValidateChangePassword(currentPassword, newPassword, confirmPassword))
			{
				return View();
			}

			try
			{
				if (MembershipService.ChangePassword(User.Identity.Name, currentPassword, newPassword))
				{
					return RedirectToAction("ChangePasswordSuccess");
				}
				else
				{
					ModelState.AddModelError("_FORM", "The current password is incorrect or the new password is invalid.");
					return View();
				}
			}
			catch
			{
				ModelState.AddModelError("_FORM", "The current password is incorrect or the new password is invalid.");
				return View();
			}
		}

		public ActionResult ChangePasswordSuccess()
		{

			return View();
		}

		protected override void OnActionExecuting(ActionExecutingContext filterContext)
		{
			if (filterContext.HttpContext.User.Identity is WindowsIdentity)
			{
				throw new InvalidOperationException("Windows authentication is not supported.");
			}
		}

		#region Validation Methods

		private bool ValidateChangePassword(string currentPassword, string newPassword, string confirmPassword)
		{
			if (String.IsNullOrEmpty(currentPassword))
			{
				ModelState.AddModelError("currentPassword", "You must specify a current password.");
			}
			if (newPassword == null || newPassword.Length < MembershipService.MinPasswordLength)
			{
				ModelState.AddModelError("newPassword",
					String.Format(CultureInfo.CurrentCulture,
						 "You must specify a new password of {0} or more characters.",
						 MembershipService.MinPasswordLength));
			}

			if (!String.Equals(newPassword, confirmPassword, StringComparison.Ordinal))
			{
				ModelState.AddModelError("_FORM", "The new password and confirmation password do not match.");
			}

			return ModelState.IsValid;
		}

		private bool ValidateLogOn(string userName, string password)
		{
			if (String.IsNullOrEmpty(userName))
			{
				ModelState.AddModelError("username", "You must specify a username.");
			}
			if (String.IsNullOrEmpty(password))
			{
				ModelState.AddModelError("password", "You must specify a password.");
			}
			if (!MembershipService.ValidateUser(userName, password))
			{
				ModelState.AddModelError("_FORM", "The username or password provided is incorrect.");
			}

			return ModelState.IsValid;
		}

		private bool ValidateRegistration(string userName, string email, string password, string confirmPassword)
		{
			if (String.IsNullOrEmpty(userName))
			{
				ModelState.AddModelError("username", "You must specify a username.");
			}
            if (userName.Length > 20)
            {
                ModelState.AddModelError("username", "Username cannot be greater than 20 characters.");
            }
			if (String.IsNullOrEmpty(email))
			{
				ModelState.AddModelError("email", "You must specify an email address.");
			}
            else if (!Regex.IsMatch(email, @"^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$"))
            {
                ModelState.AddModelError("email", "Invalid email address.");
            }
            else if (email.Length > 50)
            {
                ModelState.AddModelError("email", "Email cannot be greater than 70 characters.");
            }
			if (password == null || password.Length < MembershipService.MinPasswordLength)
			{
				ModelState.AddModelError("password",
					String.Format(CultureInfo.CurrentCulture,
						 "You must specify a password of {0} or more characters.",
						 MembershipService.MinPasswordLength));
			}
			if (!String.Equals(password, confirmPassword, StringComparison.Ordinal))
			{
				ModelState.AddModelError("_FORM", "The new password and confirmation password do not match.");
			}

            // Validate avatar image upload
			if (Request.Files.Count < 1)
			{
				ModelState.AddModelError("avatarUpload", "You must provide an avatar");
			}			
			if (!this.HasImageExtention(Request.Files[0].FileName))
            {
                StringBuilder errorString = new StringBuilder();

                errorString.Append("Upload an image file with one of these extentions: ");

                foreach (string ext in this.validExtentions)
                {
                    errorString.Append(ext + ", ");
                }

                //   Remove last comma / space
                errorString.Remove(errorString.Length - 2, 2);

                ModelState.AddModelError("avatarUpload", errorString.ToString());
            }
            else if (!this.IsValidAvatarSize(Request.Files[0].ContentLength))
            {
                ModelState.AddModelError("avatarUpload", "Avatar upload must be less than 8MB");
            }

			return ModelState.IsValid;
		}

        private bool IsValidAvatarSize(int size)
        {
            //  Between 0 and 4 MB
            return (0 < size) && (size < (8 * MB));
        }

        private readonly int MB = 1024 * 1024;

        private bool HasImageExtention(string filename)
        {
			bool hasExtension = false;

			if (!String.IsNullOrEmpty(filename))
			{
				//	Get extention and remove leading ."
				string ext = Path.GetExtension(filename).Trim().ToLower().Remove(0, 1);

				hasExtension = validExtentions.Contains(ext);
			}

			return hasExtension;
        }

        private List<string> validExtentions = new List<string>() 
        { 
                "gif", 
                "jpg", 
                "jpeg", 
                "png", 
                "bmp"  
        };

		private static string ErrorCodeToString(MembershipCreateStatus createStatus)
		{
			// See http://msdn.microsoft.com/en-us/library/system.web.security.membershipcreatestatus.aspx for
			// a full list of status codes.
			switch (createStatus)
			{
				case MembershipCreateStatus.DuplicateUserName:
					return "Username already exists. Please enter a different user name.";

				case MembershipCreateStatus.DuplicateEmail:
					return "A username for that e-mail address already exists. Please enter a different e-mail address.";

				case MembershipCreateStatus.InvalidPassword:
					return "The password provided is invalid. Please enter a valid password value.";

				case MembershipCreateStatus.InvalidEmail:
					return "The e-mail address provided is invalid. Please check the value and try again.";

				case MembershipCreateStatus.InvalidAnswer:
					return "The password retrieval answer provided is invalid. Please check the value and try again.";

				case MembershipCreateStatus.InvalidQuestion:
					return "The password retrieval question provided is invalid. Please check the value and try again.";

				case MembershipCreateStatus.InvalidUserName:
					return "The user name provided is invalid. Please check the value and try again.";

				case MembershipCreateStatus.ProviderError:
					return "The authentication provider returned an error. Please verify your entry and try again. If the problem persists, please contact your system administrator.";

				case MembershipCreateStatus.UserRejected:
					return "The user creation request has been canceled. Please verify your entry and try again. If the problem persists, please contact your system administrator.";

				default:
					return "An unknown error occurred. Please verify your entry and try again. If the problem persists, please contact your system administrator.";
			}
		}
		#endregion
	}

	// The FormsAuthentication type is sealed and contains static members, so it is difficult to
	// unit test code that calls its members. The interface and helper class below demonstrate
	// how to create an abstract wrapper around such a type in order to make the AccountController
	// code unit testable.

	public interface IFormsAuthentication
	{
		void SignIn(string userName, bool createPersistentCookie);
		void SignOut();
	}

	public class FormsAuthenticationService : IFormsAuthentication
	{
		public void SignIn(string userName, bool createPersistentCookie)
		{
            FormsAuthentication.SetAuthCookie(userName, false); //createPersistentCookie);
		}
		public void SignOut()
		{
			FormsAuthentication.SignOut();
		}
	}

	public interface IMembershipService
	{
		int MinPasswordLength { get; }

		bool ValidateUser(string userName, string password);
		MembershipCreateStatus CreateUser(string userName, string password, string email);
		bool ChangePassword(string userName, string oldPassword, string newPassword);
	}

	public class AccountMembershipService : IMembershipService
	{
		private MembershipProvider _provider;

		public AccountMembershipService()
			: this(null)
		{
		}

		public AccountMembershipService(MembershipProvider provider)
		{
			_provider = provider ?? Membership.Provider;
		}

		public int MinPasswordLength
		{
			get
			{
				return _provider.MinRequiredPasswordLength;
			}
		}

		public bool ValidateUser(string userName, string password)
		{
			return _provider.ValidateUser(userName, password);
		}

		public MembershipCreateStatus CreateUser(string userName, string password, string email)
		{
			MembershipCreateStatus status;
			_provider.CreateUser(userName, password, email, null, null, true, null, out status);
			return status;
		}

		public bool ChangePassword(string userName, string oldPassword, string newPassword)
		{
			MembershipUser currentUser = _provider.GetUser(userName, true /* userIsOnline */);
			return currentUser.ChangePassword(oldPassword, newPassword);
		}
	}
}
